import os
import threading
from collections import defaultdict
from typing import List, Dict, Optional
import jieba

class FileUtil:
    """
    文件操作工具类：提供读取文件的方法
    """
    @staticmethod
    def read_file(file_path: str) -> Optional[str]:
        """
        读取文件内容
        - 参数：
            - file_path: 文件路径
        返回值：
            - 文件内容（字符串），若读取失败返回None
        """
        try:
            with open(file_path, 'r', encoding='utf-8') as f:
                return f.read()
        except Exception as e:
            print(f"Error reading file {file_path}: {e}")
            return None


class StringUtil:
    """
    字符串工具类：提供字符串分割功能
    """
    @staticmethod
    def split(target: str, sep: str) -> List[str]:
        """
        按指定分隔符拆分字符串
        - 参数：
            - target: 待拆分的字符串
            - sep: 分隔符
        - 返回值：
            - 字符串列表
        """
        return target.split(sep)


class JiebaUtil:
    """
    Jieba分词工具类（单例模式），同时支持加载停用词表
    """
    _instance = None
    _lock = threading.Lock()

    def __init__(self):
        self.jieba = jieba
        self.stop_words = set()  # 存储停用词的集合
        self._load_stop_words("dict/stop_words.utf8")

    def _load_stop_words(self, stop_words_path: str):
        """
        加载停用词表
        - 参数：
            - stop_words_path: 停用词表文件路径
        """
        try:
            with open(stop_words_path, 'r', encoding='utf-8') as f:
                for line in f:
                    self.stop_words.add(line.strip())
        except Exception as e:
            print(f"Error loading stop words: {e}")

    @classmethod
    def get_instance(cls):
        """
        获取JiebaUtil的单例实例
        """
        if not cls._instance:
            with cls._lock:
                if not cls._instance:
                    cls._instance = JiebaUtil()
        return cls._instance

    def cut_string(self, src: str) -> List[str]:
        """
        对字符串进行分词，并过滤停用词
        - 参数：
            - src: 输入字符串
        - 返回值：
            - 分词结果列表
        """
        words = self.jieba.cut_for_search(src)
        return [word for word in words if word not in self.stop_words]


class DocInfo:
    """
    文档信息类：存储文档的标题、内容、URL及其唯一标识符
    """
    def __init__(self, title: str, content: str, url: str, doc_id: int):
        self.title = title      # 文档标题
        self.content = content  # 文档内容
        self.url = url          # 文档URL
        self.doc_id = doc_id    # 文档唯一ID


class InvertedElem:
    """
    倒排索引元素类：记录单词所在文档及其权重
    """
    def __init__(self, doc_id: int, word: str, weight: int):
        self.doc_id = doc_id  # 文档ID
        self.word = word      # 单词
        self.weight = weight  # 权重


class Index:
    """
    索引管理类（单例模式）：维护正排索引和倒排索引
    """
    _instance = None
    _lock = threading.Lock()

    def __init__(self):
        self.forward_index: List[DocInfo] = []                                  # 正排索引：存储文档信息列表
        self.inverted_index: Dict[str, List[InvertedElem]] = defaultdict(list)  # 倒排索引：单词映射到倒排列表

    @classmethod
    def get_instance(cls):
        """
        获取Index的单例实例
        """
        if not cls._instance:
            with cls._lock:
                if not cls._instance:
                    cls._instance = Index()
        return cls._instance

    def get_forward_index(self, doc_id: int) -> Optional[DocInfo]:
        """
        根据文档ID获取正排索引中的文档信息
        - 参数：
            - doc_id: 文档ID
        - 返回值：
            - 文档信息对象，若ID无效返回None
        """
        if 0 <= doc_id < len(self.forward_index):
            return self.forward_index[doc_id]
        else:
            print(f"doc_id {doc_id} out of range!")
            return None

    def get_inverted_list(self, word: str) -> Optional[List[InvertedElem]]:
        """
        获取单词的倒排列表
        - 参数：
            - word: 单词
        - 返回值：
            - 倒排列表，若单词不存在返回None
        """
        if word in self.inverted_index:
            return self.inverted_index[word]
        else:
            print(f"No inverted list for word: {word}")
            return None

    def build_index(self, input_path: str) -> bool:
        """
        构建正排索引和倒排索引
        - 参数：
            - input_path: 输入数据文件路径
        - 返回值：
            - 构建成功返回True，否则返回False
        """
        try:
            with open(input_path, 'r', encoding='utf-8') as f:
                for line in f:
                    # 构建正排索引
                    doc = self._build_forward_index(line)
                    if doc:
                        # 构建倒排索引
                        self._build_inverted_index(doc)
            return True
        except Exception as e:
            print(f"Error building index: {e}")
            return False

    def _build_forward_index(self, line: str) -> Optional[DocInfo]:
        """
        构建正排索引条目
        - 参数：
            - line: 输入的一行数据
        - 返回值：
            - 构建的文档信息对象，若格式无效返回None
        """
        parts = StringUtil.split(line, '\u0003')
        if len(parts) != 3:
            print(f"Invalid line format: {line}")
            return None

        # 清理URL中的多余字符
        url = parts[2].strip()
        doc_id = len(self.forward_index)  # 新文档的ID
        doc = DocInfo(parts[0], parts[1], url, doc_id)
        self.forward_index.append(doc)
        return doc

    def _build_inverted_index(self, doc: DocInfo):
        """
        构建倒排索引条目
        - 参数：
            - doc: 正排索引中的文档信息对象
        """
        word_map = defaultdict(lambda: {'title_cnt': 0, 'content_cnt': 0})

        # 分析标题中的词频
        title_words = JiebaUtil.get_instance().cut_string(doc.title)
        for word in title_words:
            word_map[word]['title_cnt'] += 1

        # 分析内容中的词频
        content_words = JiebaUtil.get_instance().cut_string(doc.content)
        for word in content_words:
            word_map[word]['content_cnt'] += 1

        # 计算权重并更新倒排索引
        X, Y = 10, 1  # 标题词频的权重系数为10，内容词频的权重系数为1
        for word, counts in word_map.items():
            weight = X * counts['title_cnt'] + Y * counts['content_cnt']
            self.inverted_index[word].append(InvertedElem(doc.doc_id, word, weight))
